package edu.uis.project.frankszoo.model.deck;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import edu.uis.project.frankszoo.model.card.Card;
import edu.uis.project.frankszoo.model.card.Joker;
import edu.uis.project.frankszoo.model.card.Mosquito;

/**
 * PlayerDeck class represents the deck of cards for a player in the game. It includes both cards in hand as well as trick of hands.
 */
public class PlayerDeck {

	protected List<Card> deckInHand; // List containing all cards currently in the hand of the player
	protected List<Card> trickOfCards;// List of cards that the player has got rid of by out ranking others in the game.
	protected HashMap<String, Integer> deckInHandStatMap; // HashMap containing CardName and CardCount statistics

	/**
	 * Constructor for PlayerDeck class: Creates two array lists for deckInHand and trickOfCards
	 */
	public PlayerDeck() {
		this.deckInHand = new ArrayList<Card>();
		this.trickOfCards = new ArrayList<Card>();
		buildStatMap();
	}

	/**
	 * This method sets the deck of cards in the hand for a player. It invokes the buildStatMap() method to update the card statistics
	 * 
	 * @param deckInHand
	 *            List containing all cards currently in the hand of the player
	 */
	public void setDeckInHand(List<Card> deckInHand) {
		this.deckInHand = deckInHand;
		buildStatMap();
	}

	/**
	 * This method removes cards from the deck of cards in hand. After removal, it invokes the buildStatMap() method to update the card statistics
	 * 
	 * @param cardsRemoved
	 *            List of cards that have been played and hence need to be removed from the players deck
	 */
	public synchronized void removeFromDeckInHand(List<Card> cardsRemoved) {
		for (Card removedCard : cardsRemoved) {
			for (int i = 0; i < deckInHand.size(); i++) {
				if (deckInHand.get(i).toString().equals(removedCard.toString())) {
					deckInHand.remove(i);
					break;
				}
			}
		}
		buildStatMap();
	}

	/**
	 * This method adds the trick of cards to the Array list 'trickOfCards'
	 * 
	 * @param cardsAdded
	 *            List of cards added to the trick of cards
	 */
	public void addToTrickOfCards(List<Card> cardsAdded) {
		trickOfCards.addAll(cardsAdded);
	}

	/**
	 * This method returns the array list of cards in deck for the hand
	 * 
	 * @returns List of cards in deck
	 */
	public List<Card> getDeckInHand() {
		return this.deckInHand;
	}

	/**
	 * This method returns the array list of cards won by tricks
	 * 
	 * @returns List of trickOfCards
	 */
	public List<Card> getTrickOfCards() {
		return this.trickOfCards;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#toString()
	 */
	public String toString() {
		return ("" + deckInHand);
	}

	/**
	 * This method returns the HashMap containing cardType and card count stats
	 * 
	 * @return HashMap containing cardType and card count stats
	 */
	public HashMap<String, Integer> getDeckInHandStatMap() {
		return deckInHandStatMap;
	}

	/**
	 * This method builds the deckInHandStatMap which contains CardName and CardCount as (key, value) pair. This is useful for running various
	 * analysis
	 */
	public void buildStatMap() {
		deckInHandStatMap = new HashMap<String, Integer>();
		for (Card card : deckInHand) {
			if (deckInHandStatMap.get(card.toString()) != null) {
				// Already in map, increase the count
				Integer count = deckInHandStatMap.get(card.toString());
				deckInHandStatMap.put(card.toString(), count + 1);
			} else {
				// Not in map. Populate the count as 1
				deckInHandStatMap.put(card.toString(), new Integer(1));
			}
		}
	}

	/**
	 * This method examines the deckInHand and returns whether a Joker is present
	 * 
	 * @return boolean Indicates whether deckInHand contains Joker
	 */
	public boolean hasJoker() {
		boolean hasJoker = false;
		if (deckInHandStatMap.containsKey("Joker")) {
			hasJoker = true;
		}
		return hasJoker;
	}

	/**
	 * This method examines the deckInHand and returns whether a Mosquito is present
	 * 
	 * @return boolean Indicates whether deckInHand contains Mosquito
	 */
	public boolean hasMosquito() {
		boolean hasMosquito = false;
		if (deckInHandStatMap.containsKey("Mosquito")) {
			hasMosquito = true;
		}
		return hasMosquito;
	}

	/**
	 * This method examines the deckInHand and returns whether either a Joker or a Mosquito is present
	 * 
	 * @return boolean Indicates whether a Joker or a Mosquito is present
	 */
	public boolean hasSpecialCards() {
		boolean hasSpecial = false;
		if (hasJoker() || hasMosquito()) {
			hasSpecial = true;
		}
		return hasSpecial;
	}

	/**
	 * This is the AI logic which picks the cards to be played if this is the first player in the round. Basically we try to get rid of the cards
	 * which occur the most. We try to keep the Joker and Mosquito for later use. As much as possible.
	 * 
	 * @return List<Card> List of cards that will be played
	 */
	public List<Card> findOptimalFirstPlay() {
		ArrayList<String> keyList = new ArrayList<String>(deckInHandStatMap.keySet());
		Integer maxValue = 0;
		String optimalCard = null;

		// We will try to avoid special cards Joker and Mosquito in the first play
		for (String key : keyList) {
			if ((key != "Joker") && (key != "Mosquito")) {
				Integer value = deckInHandStatMap.get(key);

				// Find the card which occurs the maximum number of times as the optimal card
				if (value > maxValue) {
					optimalCard = key;
					maxValue = value;
				}
			}
		}

		List<Card> cardsPlayed = null;
		if (optimalCard != null) {
			// Pick all the cards that are present so we can get rid of them at the earliest
			for (Card card : deckInHand) {
				if (card.canPlayAlone() && card.toString().equals(optimalCard)) {
					cardsPlayed = new ArrayList<Card>();
					for (int i = 0; i < maxValue; i++) {
						cardsPlayed.add(card);
					}
					break;
				}
			}
		} else {
			// If we did not get any cards we most likely only have mosquito left. We fall back to default strategy of picking first card
			for (Card card : deckInHand) {
				if (card.canPlayAlone()) {
					cardsPlayed = new ArrayList<Card>();
					cardsPlayed.add(card);
					break;
				}
			}
		}

		// Special case to prevent only Joker left behind in the end
		if (preventJokerLeftBehind(cardsPlayed)) {
			// Add the joker to the played Cards
			cardsPlayed.add(new Joker());
		}
		return cardsPlayed;
	}

	/**
	 * This is the AI logic which picks the cards to be played if this is intermediate player in the round. Basically we try to get rid of the cards
	 * which occur the most. We have two ways to outrank. Either by using one more card of the same type Or by using same number of outranking card.
	 * We will use use the Joker or Mosquito appropriately to make up numbers if we fall short
	 * 
	 * @param activeCardSet
	 *            The list of current active card set
	 * @return List<Card> List of cards that will be played
	 */
	public List<Card> findOptimalSubsequentPlay(List<Card> activeCardSet) {
		List<Card> cardsPlayed = null;
		int activeCardCount = activeCardSet.size(); // No of cards in the activeCardSet
		// Determine the current active card type
		Card activeCardType = extractCardType(activeCardSet);

		if (deckInHandStatMap.containsKey(activeCardType.toString())) {
			// Card type exists in player deck
			int deckCardCount = deckInHandStatMap.get(activeCardType.toString());

			// Check if more cards than activeCardCount
			if (deckCardCount > activeCardCount) {
				// Outrank by one more card of same type
				cardsPlayed = new ArrayList<Card>();
				for (int i = 0; i <= activeCardCount; i++) {
					cardsPlayed.add(activeCardType);
				}
			} else if (activeCardType.toString().equals("El") && hasMosquito() && (deckCardCount + 1) > activeCardCount) {
				// Add the mosquito to add up the numbers
				cardsPlayed = new ArrayList<Card>();
				for (int i = 0; i < activeCardCount; i++) {
					cardsPlayed.add(activeCardType);
				}
				for (Card card : deckInHand) {
					if (card instanceof Mosquito) {
						cardsPlayed.add(card);
						break;
					}
				}
			} else if (hasJoker() && (deckCardCount + 1) > activeCardCount) {
				// Try to use the joker to add up the numbers
				cardsPlayed = new ArrayList<Card>();
				for (int i = 0; i < activeCardCount; i++) {
					cardsPlayed.add(activeCardType);
				}
				for (Card card : deckInHand) {
					if (card instanceof Joker) {
						cardsPlayed.add(card);
						break;
					}
				}
			}
		}

		if (cardsPlayed == null) {
			// We did not outrank with same card type now let us try to find outranking cards
			List<Card> outrankingCards = new ArrayList<Card>();

			// Build a list of outranking cards
			for (Card card : deckInHand) {
				if (card.canOutrank(activeCardType)) {
					outrankingCards.add(card);
				}
			}

			// Iterate through the outranking cards until one that meets the criteria is found
			for (Card outrankingCard : outrankingCards) {
				Integer cardCount = deckInHandStatMap.get(outrankingCard.toString());
				if (cardCount >= activeCardCount) {
					// We have enough cards to outrank
					cardsPlayed = new ArrayList<Card>();
					for (int i = 0; i < activeCardCount; i++) {
						cardsPlayed.add(outrankingCard);
					}
				} else if (activeCardType.toString().equals("El") && hasMosquito() && (cardCount + 1) == activeCardCount) {
					// Add the mosquito to add up the numbers
					cardsPlayed = new ArrayList<Card>();
					for (int i = 0; i < cardCount; i++) {
						cardsPlayed.add(outrankingCard);
					}
					for (Card card : deckInHand) {
						if (card instanceof Mosquito) {
							cardsPlayed.add(card);
							break;
						}
					}
				} else if (hasJoker() && (cardCount + 1) == activeCardCount) {
					// Try to use the joker to add up the numbers
					cardsPlayed = new ArrayList<Card>();
					for (int i = 0; i < cardCount; i++) {
						cardsPlayed.add(outrankingCard);
					}
					for (Card card : deckInHand) {
						if (card instanceof Joker) {
							cardsPlayed.add(card);
							break;
						}
					}
				} else {
					// Tough luck we will have to pass
				}
			}
		}

		// Special case to prevent only Joker left behind
		if (preventJokerLeftBehind(cardsPlayed)) {
			cardsPlayed = null;
			System.out.println("\t\tWill not play as only Joker will be left behind!");
		}

		return cardsPlayed;
	}

	/**
	 * This method determines whether after the card are played, only a Joker is left behind. While this is allowed move. It will leave the player
	 * stuck with the joker as it cannot be played on its own
	 * 
	 * @param cardsPlayed
	 *            List of cards played
	 * @return boolean Indicates whether joker will be left behind
	 */
	private boolean preventJokerLeftBehind(List<Card> cardsPlayed) {
		boolean jokerLeftBehind = false;
		if (cardsPlayed != null) {
			List<Card> cloneCardSet = new ArrayList<Card>();

			// We clone the deckInHand
			for (Card card : deckInHand) {
				cloneCardSet.add(card);
			}

			// Remove the cards played from the cloneCardSet to get the future deck
			for (Card removedCard : cardsPlayed) {
				for (int i = 0; i < cloneCardSet.size(); i++) {
					if (cloneCardSet.get(i).toString().equals(removedCard.toString())) {
						cloneCardSet.remove(i);
						break;
					}
				}
			}

			if (cloneCardSet.size() == 1) {
				// Check if Joker is left behind
				if (cloneCardSet.get(0) instanceof Joker) {
					// Cannot play as Joker will be only left behind
					jokerLeftBehind = true;
				}
			}
		}
		return jokerLeftBehind;
	}

	/**
	 * This method determines the type of card played by the outranking player. This is needed because an outranking set could be a mix of joker and
	 * mosquito.
	 * 
	 * @param activeCardSet
	 *            List of cards in the activeCardSet
	 * @return Card This card is the overall type of the card in the activeCardSet
	 */
	private Card extractCardType(List<Card> activeCardSet) {
		Card activeCardType = null;
		boolean hasSpecialCards = false;

		// Determine whether the activeCardSet has SpecialCards
		for (Card card : activeCardSet) {
			if (card instanceof Joker || card instanceof Mosquito) {
				hasSpecialCards = true;
				break;
			}
		}
		if (hasSpecialCards) {
			// Special cards will be eliminated to determine the type of the remaining cards
			List<Card> cloneCardSet = new ArrayList<Card>();

			// We use a clone of the activeCardSet so as to not disturb the original list.
			for (Card card : activeCardSet) {
				if (!(card instanceof Joker) && !(card instanceof Mosquito)) {
					cloneCardSet.add(card);
				}
			}

			if (cloneCardSet.size() > 0) {
				// If there are cards left then Joker and Mosquito have been eliminated so the card type will be that of the remaining card
				activeCardType = cloneCardSet.get(0);
			} else {
				// If no cards were left then active card set contained Mosquito which got removed.
				activeCardType = new Mosquito();
			}

		} else {
			// There were no special cards. Hence the card type will be that of any of the cards in the list.
			activeCardType = activeCardSet.get(0);
		}

		return activeCardType;
	}
}
